﻿
/*
 * These classes were based around the FastSerializer toolset (http://www.codeproject.com/KB/cs/FastSerialization.aspx)(May 2006)  by Mr. Tim Haynes, who has our gratitude! *
 */

using System;
using System.IO;
using System.Reflection;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Linq;
using System.Runtime.InteropServices;
using System.Collections;

namespace Liquifier
{
    /// <summary> SerialisationReader.  Extends BinaryReader to add additional data types,
    /// handle null strings and simplify use with ISerializable. </summary>

    public class SerialisationReader : BinaryReader
    {
        public SerialisationReader(Stream s) : base(s) { }

        /// <summary> Reads a string from the buffer.  Overrides the base implementation so it can cope with nulls. </summary>

        public override string ReadString()
        {
            ObjType t = (ObjType)ReadByte();
            if (t == ObjType.stringType) return base.ReadString();
            return null;
        }

        /// <summary> Reads a byte array from the buffer, handling nulls and the array length. </summary>

        public byte[] ReadByteArray()
        {
            int len = ReadInt32();
            if (len > 0) return base.ReadBytes(len);
            if (len < 0) return null;
            return new byte[0];
        }

        public override int Read(byte[] buffer, int index, int count)
        {
            throw new Exception("Do not use this method. Use ReadByteArray() instead.");
        }

        public override byte[] ReadBytes(int count)
        {
            throw new Exception("Do not use this method. Use ReadByteArray() instead.");
        }

        /// <summary> Reads a char array from the buffer, handling nulls and the array length. </summary>

        public char[] ReadCharArray()
        {
            int len = ReadInt32();
            if (len > 0) return ReadChars(len);
            if (len < 0) return null;
            return new char[0];
        }

        /// <summary> Reads a DateTime from the buffer. </summary>

        public DateTime ReadDateTime() { return new DateTime(ReadInt64()); }

        /// <summary> Reads a generic Dictionary from the buffer. </summary>

        public IDictionary<T, U> ReadDictionary<T, U>(SolidificationContext context)
        {
            int count = ReadInt32();
            if (count < 0) return null;
            IDictionary<T, U> d = new Dictionary<T, U>();
            for (int i = 0; i < count; i++) d[(T)ReadObject(context, -1)] = (U)ReadObject(context, -1);
            return d;
        }

        /// <summary> Reads an object which was added to the buffer by WriteObject. </summary>

        public object ReadObject(SolidificationContext context, int referenceToAddAs)
        {
            ObjType t = (ObjType)ReadByte();
            switch (t)
            {
                case ObjType.boolType: return ReadBoolean();
                case ObjType.byteType: return ReadByte();
                case ObjType.uint16Type: return ReadUInt16();
                case ObjType.uint32Type: return ReadUInt32();
                case ObjType.uint64Type: return ReadUInt64();
                case ObjType.sbyteType: return ReadSByte();
                case ObjType.int16Type: return ReadInt16();
                case ObjType.int32Type: return ReadInt32();
                case ObjType.int64Type: return ReadInt64();
                case ObjType.charType: return ReadChar();
                case ObjType.stringType: return base.ReadString();
                case ObjType.singleType: return ReadSingle();
                case ObjType.doubleType: return ReadDouble();
                case ObjType.decimalType: return ReadDecimal();
                case ObjType.dateTimeType: return ReadDateTime();
                case ObjType.byteArrayType: return ReadByteArray();
                case ObjType.charArrayType: return ReadCharArray();
                case ObjType.otherGenericArray: return ReadGenericArray(context, referenceToAddAs);
                case ObjType.otherGenericCollection: return ReadGenericCollection(context, referenceToAddAs);
                case ObjType.otherLiquifiableType: return Liquifer.Solidify(BaseStream, context); //referenceToAddAs is not passed here, because Solidify() works out the reference to add as on its own.
                case ObjType.otherBinaryFormattedType: return new BinaryFormatter().Deserialize(BaseStream);
                case ObjType.NewReference: return ReadLiquifiedObject(context);
                case ObjType.ExistingReference: return ReadReferencedObject(context);
                case ObjType.otherGenericValueType: return ReadOtherGenericValueType();
                case ObjType.delegateType: return ReadGenericDelegate(context);
                case ObjType.nullType: return null;

                default:
                    throw new Exception("Encountered unknown object/type identifier in stream - cannot continue deserialisation as it is not known how without this ID.");
            }
        }

        internal object ReadOtherGenericValueType()
        {
            TypeData typeInfo = TypeData.GetTypeData(ReadString());

            var valueData = ReadByteArray();

            GCHandle Hdata = GCHandle.Alloc(valueData, GCHandleType.Pinned);
            return Marshal.PtrToStructure(Hdata.AddrOfPinnedObject(), typeInfo.Type);
        }

        internal object ReadReferencedObject(SolidificationContext context)
        {
            Int32 id = ReadInt32();
            return context.ReferenceItems[id];
        }

        internal object ReadLiquifiedObject(SolidificationContext context)
        {
            Int32 id = ReadInt32();
            object o = ReadObject(context, id);

            if (context.ReferenceItems.ContainsKey(id))
            {
                if (context.ReferenceItems[id] != o)
                {
                    throw new Exception("Object Mismatch: An object in the stream has been given the same ID as a previous object.");
                }
            }
            else
            {
                context.ReferenceItems.Add(id, o);
            }
            return o;
        }

        internal object ReadGenericArray(SolidificationContext context, int referenceToAddAs)
        {
            TypeData array_type = TypeData.GetTypeData(ReadString());
            TypeData element_type = TypeData.GetTypeData(ReadString());

            int count = ReadInt32();

            Array a = Array.CreateInstance(element_type.Type, count);

            if (referenceToAddAs >= 0)
            {
                context.ReferenceItems.Add(referenceToAddAs, a);
            }

            if (count > 0)
            {
                if (element_type.IsBlittable)
                {
                    //allocated beforehand because when we used the NonBlittable exception to test if a type was blittable, if it threw we didn't want to have 
                    //read a load of data and thus mess up the stream...there is no point to this comment other than a reminder of how it used to be done so
                    //no one goes changing this for no reason.

                    GCHandle h = GCHandle.Alloc(a, GCHandleType.Pinned);
                    int arrayDataSize = Marshal.SizeOf(element_type.Type) * count;
                    var array_data = ReadByteArray();
                    Marshal.Copy(array_data, 0, h.AddrOfPinnedObject(), arrayDataSize);
                    h.Free();

                    return a;
                }

                for (int i = 0; i < count; i++)
                {
                    a.SetValue(ReadObject(context, -1), i);
                }
            }

            return a;           
        }

        internal object ReadGenericCollection(SolidificationContext context, int referenceToAddAs)
        {
            TypeData collection_type = TypeData.GetTypeData(ReadString());
            TypeData element_type = TypeData.GetTypeData(ReadString());

            int count = ReadInt32();

            if (collection_type.Type.IsArray)
            {
                Array a = Array.CreateInstance(element_type.Type, count);
                if (referenceToAddAs >= 0)
                {
                    context.ReferenceItems.Add(referenceToAddAs, a);
                }

                if (count > 0)
                {
                    if (element_type.IsBlittable)
                    {
                        //allocated beforehand because when we used the NonBlittable exception to test if a type was blittable, if it threw we didn't want to have 
                        //read a load of data and thus mess up the stream...there is no point to this comment other than a reminder of how it used to be done so
                        //no one goes changing this for no reason.

                        GCHandle h = GCHandle.Alloc(a, GCHandleType.Pinned);
                        int arrayDataSize = Marshal.SizeOf(element_type.Type) * count;
                        var array_data = ReadByteArray();
                        Marshal.Copy(array_data, 0, h.AddrOfPinnedObject(), arrayDataSize);
                        h.Free();

                        return a;
                    }

                    for (int i = 0; i < count; i++)
                    {
                        a.SetValue(ReadObject(context, -1), i);
                    }
                }

                return a;
            }
            else
            {
                IList o = collection_type.CreateInstance() as IList;
                if (referenceToAddAs >= 0)
                {
                    context.ReferenceItems.Add(referenceToAddAs, o);
                }

                if (count > 0)
                {
                    for (int i = 0; i < count; i++)
                    {
                        o.Add(ReadObject(context, -1));
                    }
                }

                return o;
            }
        }

        internal object ReadGenericDelegate(SolidificationContext context)
        {
            int invocationCount = ReadInt32();
            
            List<Delegate> invocationList = new List<Delegate>();
            for (int i = 0; i < invocationCount; i++)
            {
                invocationList.Add(ReadDelegate(context));
            }

            return MulticastDelegate.Combine(invocationList.ToArray());
        }

        internal Delegate ReadDelegate(SolidificationContext context)
        {
            object target = ReadObject(context, -1);

            TypeData delegateType = TypeData.GetTypeData(ReadString());
   
            string methodName = ReadString();
            string methodReturnTypeName = ReadString();

            int methodParameterCount = ReadInt32();
            List<string> methodParameterNames = new List<string>();
            for(int i = 0; i < methodParameterCount; i++)
            {
                methodParameterNames.Add(ReadString());
            }

            MethodInfo method = FindMethod(target, methodName, methodReturnTypeName, methodParameterNames.ToArray());

            Delegate d = Delegate.CreateDelegate(delegateType.Type, target, method);
            return d;
        }

        internal MethodInfo FindMethod(object o, string name, string returntypename, string[] parametertypenames)
        {
            MethodInfo[] methods = o.GetType().GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance);
            foreach (MethodInfo m in methods)
            {
                if (m.Name == name)
                {
                    if (m.ReturnType.Name == returntypename)
                    {
                        if (DoParametersMatch(parametertypenames, m.GetParameters()))
                            return m;
                    }
                }
            }

            throw new Exception(string.Format("Could not find method ({0} {1}({2})) in object of type {3}.", returntypename, name, String.Join(", ", parametertypenames), o.GetType().Name));
        }

        internal bool DoParametersMatch(string[] parametertypenames, ParameterInfo[] parameters)
        {
            if (parametertypenames.Length != parameters.Length)
                return false;

            for (int i = 0; i < parametertypenames.Length; i++)
            {
                if (parametertypenames[i] != parameters[i].ParameterType.Name)
                {
                    return false;
                }
            }

            return true;
        }

    } // SerialisationReader
}
